package com.chinacreator.asp.comp.sys.oauth2.sso.client.spi;

import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.chinacreator.asp.comp.sys.oauth2.OAuth2Credential;
import com.chinacreator.asp.comp.sys.oauth2.OAuth2CredentialFactory;
import com.chinacreator.asp.comp.sys.oauth2.common.Credential;
import com.chinacreator.asp.comp.sys.oauth2.common.CredentialConfiguration;
import com.chinacreator.asp.comp.sys.oauth2.common.CredentialStore;
import com.chinacreator.asp.comp.sys.oauth2.common.exceptions.InvalidTokenException;
import com.chinacreator.asp.comp.sys.oauth2.common.exceptions.LoginExpiredException;
import com.chinacreator.asp.comp.sys.oauth2.common.exceptions.UnExpectedException;
import com.chinacreator.asp.comp.sys.oauth2.common.util.CORSUtil;
import com.chinacreator.asp.comp.sys.oauth2.common.util.CookieUtil;
import com.chinacreator.asp.comp.sys.oauth2.common.util.HeaderUtil;
import com.chinacreator.asp.comp.sys.oauth2.common.util.SSOUtils;
import com.chinacreator.asp.comp.sys.oauth2.common.util.SecurityLevel;
import com.chinacreator.asp.comp.sys.oauth2.common.util.StringUtils;
import com.chinacreator.asp.comp.sys.oauth2.resourceserver.bean.UrlResource;
import com.chinacreator.asp.comp.sys.oauth2.resourceserver.cache.UrlPermissionResourceCache;
import com.chinacreator.asp.comp.sys.oauth2.resourceserver.util.ApiGatewayRequestor;
import com.chinacreator.asp.comp.sys.oauth2.resourceserver.util.URLResourceType;
import com.chinacreator.c2.sysmgr.User;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.ParameterStyle;
import org.apache.oltu.oauth2.rs.request.OAuthAccessResourceRequest;
import org.json.JSONObject;

/**
 * OAuth2统一认证授权拦截器
 *
 * @author 彭盛
 */
public class OAuth2SSOFilter implements Filter {

  public static final String ERROR_TYPE_LOGIN_EXPIRED = "login_expired";

  private SSOUtils oAuth2SSOService = SSOUtils.getOAuth2SSOService();

  @Override
  public void init(FilterConfig filterConfig) {
    CredentialConfiguration.init(filterConfig);
    if (CredentialConfiguration.isResourceAuthorizationEnabled()) {
		// 这里只是考虑了服务、页面等的基于URL的鉴权
		UrlPermissionResourceCache.getInstance().init();
	}
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    // 请求允许跨域访问
    CORSUtil.addCORSHeaders(httpRequest, httpResponse);

    String base = httpRequest.getContextPath();
    String currentURL = httpRequest.getRequestURI();
    if (!StringUtils.isEmpty(base) && !StringUtils.isEmpty(currentURL) && !"/".equals(base)) {
      currentURL = currentURL.substring(base.length());
    }

    try {
      // 判断是否匿名访问的静态资源
      if (oAuth2SSOService.isAnonUrlByStaticRes(httpRequest, currentURL)) {
        chain.doFilter(httpRequest, httpResponse);
      }
      // 判断是否配置的匿名访问路径
      else if (oAuth2SSOService.isAnonUrlByConfig(currentURL)) {
        doAonoUrlFilter(httpRequest, httpResponse, chain);
      }
      // 判断是否服务器端登录成功/失败后重定向到的客户端地址
      else if (CredentialConfiguration.OAUTH2_SSO_REDIRECTLOGIN_URL.equals(currentURL)) {
        doOAuth2LoginFilter(httpRequest, httpResponse, chain);
      }
      // 判断是否服务器端登出成功/失败后重定向到的客户端地址
      else if (CredentialConfiguration.OAUTH2_SSO_REDIRECTLOGOUT_URL.equals(currentURL)) {
        doOAuth2LogoutFilter(httpRequest, httpResponse, chain);
      }
      // 前台主动来刷新凭证
      else if (CredentialConfiguration.OAUTH2_SSO_REFRESHTOKEN_URL.equals(currentURL)) {
        doRefreshToken(httpRequest, httpResponse, chain);
      }
      // 拦截前台获取当前用户的请求
      else if (CredentialConfiguration.OAUTH2_SSO_GETSUBJECT_URL.equals(currentURL)) {
        doGetSubject(httpRequest, httpResponse, chain);
      }
      // 登出
      else if (CredentialConfiguration.OAUTH2_SSO_CLIENT_LOGOUT_URL.equals(currentURL)) {
        doClientLogout(httpRequest, httpResponse);
      }
      // 其他普通需认证才能访问的请求
      else {
        doAuthcFilter(httpRequest, httpResponse, chain);
      }
    } finally {
      // 请求完成，清除当前线程的用户凭证信息
      CredentialStore.clearCurrCredential();
    }
  }

  /**
   * 从请求中获取AT，优先级Header > Cookie
   */
  private String getAccessToken(HttpServletRequest request) {
    String accessToken = null;
    try {
      OAuthAccessResourceRequest oauthRequest =
          new OAuthAccessResourceRequest(request, ParameterStyle.HEADER);
      accessToken = oauthRequest.getAccessToken();
    } catch (OAuthSystemException e) {
    } catch (OAuthProblemException e) {
    }
    if (StringUtils.isEmpty(accessToken)) {
      // 还支持直接将at存储到cookie中
      accessToken =
          CookieUtil.getCookieByName(request, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN);
    }
    return accessToken;
  }

  /**
   * 尝试从请求中获取到当前用户的有效凭证，如果凭证超时则会去服务端换取新的凭证后再返回，更新凭证后会直接写新的cookie。 <br/>如果登录失效或者发生了错误则抛出异常。
   * <br/>返回的凭证只保证本地有效，未去服务器做远程校验。如需去服务器校验，可以调用{@link Credential#introspect()}接口
   *
   * @throws LoginExpiredException 登录已超时或没有登录
   * @throws InvalidTokenException 凭证无效的错误，比如签名有问题，aid不匹配等
   * @throws UnExpectedException 其他异常，一般是环境或配置引起的
   */
  public Credential tryGetCredential(HttpServletRequest request, HttpServletResponse response)
      throws LoginExpiredException, UnExpectedException, InvalidTokenException {
    String accessToken = getAccessToken(request);

    Credential credential = null;
    try {
      credential = OAuth2CredentialFactory
          .getCredentialByAccessToken(accessToken, HeaderUtil.isAPIRequest(request));
    } catch (NullPointerException e) {
      // 空异常不做处理
    } catch (TokenExpiredException e) {
      // token过期不做处理
    } catch (JWTVerificationException e) {
      String errorMessage = e.getMessage();
      if (!StringUtils.startsWithIgnoreCase(errorMessage, "The Token can't be used before")) {
        //时间早于签名的开始时间没有单独的异常，只能先这样判断
        throw new InvalidTokenException("凭证校验失败：" + e.getMessage(), e);
      }
    }

    if (null == credential) {
      // at不存在或失效，更新之
      String refreshToken = CookieUtil
          .getCookieByName(request, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN);
      if (StringUtils.isEmpty(refreshToken)) {
        throw new LoginExpiredException("没有找到任何凭证");
      }
      //更新凭证，如果RT超时了会直接抛出异常
      OAuth2Credential newCredential =
          OAuth2CredentialFactory.getCredentialByRefreshToken(refreshToken);
      if (null != newCredential) {
        // 换新的之后刷新前端cookie中存储的内容
        CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN,
            newCredential.getAccessToken(), -1);
        CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN,
            newCredential.getRefreshToken(), getRTMaxAge(newCredential));

        try {
          credential =
              OAuth2CredentialFactory.getCredentialByAccessToken(newCredential.getAccessToken(),
                  false);
          //正常程序这里都不应该出现异常，如果出问题了说明程序本地的配置或者环境有问题，直接拒绝访问，避免页面不停尝试重新认证
        } catch (NullPointerException e) {
          throw new UnExpectedException("从认证服务器获取的token为空，请联系管理员");
        } catch (TokenExpiredException e) {
          //时间不同步问题在非登录请求中都按登录失效处理，再次登录时会返回对应的错误
          throw new LoginExpiredException("新生成的token已过期，认证失败，很有可能是由后端各服务器之间时间不同步引起的，请联系管理员检查环境!");
        } catch (JWTVerificationException e) {
          String errorMessage = e.getMessage();
          if (StringUtils.startsWithIgnoreCase(errorMessage, "The Token can't be used before")) {
            //时间早于签名的开始时间没有单独的异常，只能先这样判断
            //时间不同步问题在非登录请求中都按登录失效处理，再次登录时会返回对应的错误
            throw new LoginExpiredException("新生成的token已过期，认证失败，很有可能是由后端各服务器之间时间不同步引起的，请联系管理员检查环境!");
          } else {
            throw new UnExpectedException("新生成的token签名校验未通过，请联系管理员检查配置，错误信息：" + errorMessage);
          }
        }
      } else {
        throw new UnExpectedException("更新用户凭证时时发生了未知错误，请稍候再试或联系管理员检查应用状态");
      }
    }
    return credential;
  }

  /**
   * 尝试从请求中获取到当前用户的有效凭证，如果凭证超时则会去服务端换取新的凭证后再返回。如果登录失效或者发生了错误则返回null
   */

  public Credential tryGetCredentialSafe(HttpServletRequest request,
      HttpServletResponse response) {
    try {
      return tryGetCredential(request, response);
    } catch (Exception e) {
    }
    return null;
  }

  /**
   * 匿名访问的路径(如果能获取到credential，把credential放入上下文中)
   */
  public void doAonoUrlFilter(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    Credential credential = tryGetCredentialSafe(request, response);
    if (credential != null) {
      CredentialStore.setCurrCredential(credential);
      CredentialConfiguration.getAuthrizationSpi().onTokenCheckSuccess(request, response, chain,
          credential);
    } else {
      chain.doFilter(request, response);
    }
  }

  /**
   * 支持前台主动使用刷新Access Token
   */
  private void doRefreshToken(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    String refreshToken = request.getParameter("refresh_token");
    if (StringUtils.isEmpty(refreshToken)) {
      refreshToken = CookieUtil.getCookieByName(request, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN);
    }
    if (StringUtils.isEmpty(refreshToken)) {
      redirectToLoginPage(request, response, chain, "", "Refresh Token为空，换取新凭证失败");
      return;
    }

    try {
      OAuth2Credential credential = OAuth2CredentialFactory
          .getCredentialByRefreshToken(refreshToken);

      // 换新的之后刷新前端cookie中存储的内容
      CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN, credential
          .getAccessToken(), -1);
      CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN, credential
          .getRefreshToken(), getRTMaxAge(credential));

      response.setContentType("application/json;charset=UTF-8");

      JSONObject result = new JSONObject();
      result.put("accessToken", credential.getAccessToken());
      result.put("refreshToken", credential.getRefreshToken());
      result.put("expires", credential.getExpires());
      result.put("reExpires", credential.getReExpires());

      response.getWriter().write(result.toString());

      response.flushBuffer();
    } catch (LoginExpiredException e) {
      redirectToLoginPage(request, response, chain, "", "Refresh Token换取新凭证失败");
    }
  }

  /**
   * 服务器端登录成功/失败后重定向到的客户端调用
   */
  private void doClientLogout(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // 换新的之后刷新前端cookie中存储的内容
    response.setContentType("application/json;charset=UTF-8");

    JSONObject result = new JSONObject();
    result.put("result", oAuth2SSOService.caculateRedirectLogoutUrl(request));
    response.getWriter().write(result.toString());
    response.flushBuffer();
  }

  /**
   * 服务器端登录成功/失败后重定向到的客户端调用
   */
  private void doOAuth2LoginFilter(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    String error = request.getParameter("errorCode");
    String errorDescription = request.getParameter("errorMessage");

    // 如果服务端返回了错误
    if (null != error && !error.trim().equals("")) {
      if (OAuthError.CodeResponse.ACCESS_DENIED.equals(error)) {
        handleLoginError(response, errorDescription,
            oAuth2SSOService.caculateRedirectLogoutUrl(request));
      } else {
        handleLoginError(response, errorDescription, null);
      }
      return;
    }

    String code = request.getParameter(OAuth.OAUTH_CODE);
    try {
      if (StringUtils.isEmpty(code)) {
        handleLoginError(response, "没有授权码", null);
      }

      OAuth2Credential credential =
          OAuth2CredentialFactory.getCredentialByCode(request.getParameter(OAuth.OAUTH_CODE));

      OAuth2CredentialFactory
          .getCredentialByAccessToken(credential.getAccessToken(), false);

      // 调用认证成功后处理
      if (credential != null) {
        CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN, credential
            .getAccessToken(), -1);
        CookieUtil.setCookie(request, response, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN,
            credential.getRefreshToken(), getRTMaxAge(credential));

        CredentialStore.setCurrCredential(credential);
        CredentialConfiguration.getAuthenticateSpi().onAuthenticateSuccessHandler(request,
            response, chain, credential);
      }
    } catch (OAuthSystemException e) {
      // 调用认证失败后处理
      CredentialConfiguration.getAuthenticateSpi().onServerFailedHandler(request, response, e,
          chain, oAuth2SSOService.caculateRedirectLogoutUrl(request));
    } catch (OAuthProblemException e) {
      handleLoginError(response, "授权码[" + code + "]无效，认证失败",
          oAuth2SSOService.caculateRedirectLoginUrl(request));
    } catch (TokenExpiredException e) {
      handleLoginError(response, "新生成的token已失效，认证失败，很有可能是由后端服务器时间晚于授权服务器时间，请联系管理员检查环境!",
          oAuth2SSOService.caculateRedirectLoginUrl(request));
    } catch (JWTVerificationException e) {
      String errorMessage = e.getMessage();
      if (StringUtils.startsWithIgnoreCase(errorMessage, "The Token can't be used before")) {
        //时间早于签名的开始时间没有单独的异常，只能先这样判断
        handleLoginError(response, "新生成的token已失效，认证失败，很有可能是由后端服务器时间早于授权服务器时间，请联系管理员检查环境!",
            oAuth2SSOService.caculateRedirectLoginUrl(request));
      } else {
        e.printStackTrace();
        handleLoginError(response, "校验新生成的Token时发生未知错误，请联系管理员检查，错误信息：" + errorMessage,
            oAuth2SSOService.caculateRedirectLoginUrl(request));
      }

    }
  }

  /**
   * 服务器端登出成功/失败后重定向到的客户端调用
   *
   * @throws IOException , ServletException
   */
  private void doOAuth2LogoutFilter(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    String error = request.getParameter("errorCode");
    String errorDescription = request.getParameter("errorMessage");

    // 如果服务端返回了错误
    if (null != error && !error.trim().equals("")) {
      handleLoginError(response, errorDescription, null);
      return;
    }

    // 登出(清除cookie)
	CookieUtil.removeCookie(request, response, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN);
	CookieUtil.removeCookie(request, response, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN);
    // 调用登出操作
    CredentialConfiguration.getAuthenticateSpi().onServerLogoutHandler(request, response, chain,
        oAuth2SSOService.caculateRedirectLoginUrl(request));
  }

  /**
   * 获取当前用户信息(包括所有的token信息)
   *
   * @throws IOException , ServletException
   */
  private void doGetSubject(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    Credential credential = null;
    try {
      credential = tryGetCredential(request, response);
    } catch (LoginExpiredException e) {
      redirectToLoginPage(request, response, chain, ERROR_TYPE_LOGIN_EXPIRED, "用户本次登录已失效，需重新认证");
      return;
    } catch (InvalidTokenException e) {
      accessDenied(response, "凭证校验失败：" + e.getMessage());
      return;
    } catch (UnExpectedException e) {
      accessDenied(response, e.getMessage());
      return;
    }

    //getSubject时去服务端检查token的有效性
    if (!credential.introspect()) {
      redirectToLoginPage(request, response, chain, ERROR_TYPE_LOGIN_EXPIRED, "用户本次登录已失效，需重新认证");
      return;
    }

    response.setContentType("application/json;charset=UTF-8");
    JSONObject result = new JSONObject(credential.getUserInfo());
    result.put("accessToken", credential.getAccessToken());
    response.getWriter().write(result.toString());
    response.flushBuffer();
  }

  /**
   * 受保护的资源访问路径调用
   */
  private void doAuthcFilter(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    Credential credential = null;
    try {
      credential = tryGetCredential(request, response);
    } catch (LoginExpiredException e) {
      redirectToLoginPage(request, response, chain, ERROR_TYPE_LOGIN_EXPIRED, "用户未登录或已超时");
      return;
    } catch (InvalidTokenException e) {
      accessDenied(response, "凭证校验失败：" + e.getMessage());
      return;
    } catch (UnExpectedException e) {
      accessDenied(response, e.getMessage());
      return;
    }
    
    CredentialStore.setCurrCredential(credential);
    
    //资源鉴权
    if (CredentialConfiguration.isResourceAuthorizationEnabled()
    		&& !doResourceAuthcFilter(request, response)) {
    	return;
    }
    
    CredentialConfiguration.getAuthrizationSpi().onTokenCheckSuccess(request, response, chain,
        credential);
  }

	private boolean doResourceAuthcFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 
			throws UnsupportedEncodingException, IOException {
	// 若无法获取当前用户中的角色信息(网关转发的其他应用的请求)已经由网关完成鉴权动作
	if (!CredentialStore.getCurrCredential().getUserInfo().containsKey("roles")) return true;

	String base = httpRequest.getContextPath();
	String currentURL = httpRequest.getRequestURI();
	if (!StringUtils.isEmpty(base) && !StringUtils.isEmpty(currentURL) && !"/".equals(base)) {
		currentURL = currentURL.substring(base.length());
	}

	UrlResource matchedResource = UrlPermissionResourceCache.getInstance().getMatchedResourceId(httpRequest);
	if(matchedResource == null) return true;

	Object[] roles = (Object[]) CredentialStore.getCurrCredential().getUserInfo().get("roles");
	// 若角色为空,也不鉴权,一般不会存在这种情况
	if(roles==null || roles.length==0) return true;

	if (!isPermitted(roles, matchedResource.getId())) {
		if(matchedResource.getType().equals(URLResourceType.INNERSERVICE)){
			forbiddenCall(httpRequest, httpResponse, "", "用户未授权服务调用权限.");
		}else{
			redirectToUnAuthorizedPage(httpRequest, httpResponse);
		}
		return false;
	}
	return true;
  }
  
	/**
	 *  未授权服务调用权限,返回403
	 * 
	 * @param request
	 * @param response
	 * @param errorType
	 * @param errorDescription
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 */
	private void forbiddenCall(HttpServletRequest request, HttpServletResponse response, String errorType,
			String errorDescription) throws UnsupportedEncodingException, IOException {
			// ajax请求或api网关转发的请求，返回设置状态码为403
			response.setStatus(HttpServletResponse.SC_FORBIDDEN);

			String userAgent = request.getHeader("User-Agent").toLowerCase();
			if ((userAgent.contains("msie") || (userAgent.contains("gecko") && userAgent.contains("rv:11")))) {
				response.setContentType("text/plain;charset=UTF-8");
			} else {
				response.setContentType("application/json;charset=UTF-8");
			}

			// 在body中带上错误的类型和描述
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("errorType", errorType);
			jsonObject.put("errorDescription", errorDescription);
			response.getOutputStream().write(jsonObject.toString().getBytes("UTF-8"));
			response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");
			response.setHeader("Pragma", "no-cache");
			response.setDateHeader("Expires", 0);
	}

	private boolean isPermitted(Object[] roles, String matchedResourceId) throws JsonParseException, JsonMappingException, IOException {
		StringBuilder queryParam = new StringBuilder();
		for (int i = 0; i < roles.length; i++) {
			queryParam.append(i==0?"?roleCode="+roles[i].toString():"&roleCode="+roles[i].toString());
		}
		ObjectMapper mapper = new ObjectMapper();
		String result = ApiGatewayRequestor.doPost("/aip/v1/permissions/verify"+queryParam.toString(),mapper.writeValueAsString(Arrays.asList(matchedResourceId)));
		JavaType jvt = mapper.getTypeFactory().constructParametricType(Map.class,String.class,Boolean.class);
		Map<String,Boolean> verifyResult = mapper.readValue(result, jvt);

		if (verifyResult != null && verifyResult.size() != 0) {
			if (verifyResult.containsKey(matchedResourceId)) {
				return verifyResult.get(matchedResourceId);
			} else {
				// 一般不会出现没有返回值,若没有则当鉴权通过
				return true;
			}
		} else {
			// 一般不会出现没有返回值,若没有则当鉴权通过
			return true;
		}
	}
	
	/**
	 * 返回未授权页面html
	 * 
	 * @param request
	 * @param response
	 * @param chain
	 * @param errorType
	 * @param errorDescription
	 * @throws IOException 
	 * @throws UnsupportedEncodingException 
	 * @throws ServletException
	 */
	public void redirectToUnAuthorizedPage(HttpServletRequest request, HttpServletResponse response) 
			throws UnsupportedEncodingException, IOException{
			// 页面请求
			// 浏览器地址栏发起的请求，返回报错页面
			response.setContentType("text/html");
			PrintWriter out = response.getWriter();
			out.println("<html lang=\"en\">");
			out.println("<head>");
			out.println("<meta charset=\"UTF-8\">");
			out.println("<title>无权访问</title>");
			out.println("</head>");
			out.println("<body>");
			out.println("<div style=\"width:100%;height:500px;display: flex;justify-content: center; align-items: center;\">");
			out.println("<div>");
			out.println("<img src=\"#\" alt=\"\" style=\"\">");
			out.println("<p style=\"text-align: center;color: #0f60be;font-weight: 500;font-size: 30px;display: block;\">抱歉，您无权访问该页面~</p>");
			out.println("<a style=\" background-color: #ffffff;border: 1px solid #0f60be;border-radius: 3px;color: #0f60be; padding: 7px 18px 7px 14px;text-align: center; text-decoration: none;  font-size: 14px;margin: auto;\" href=\"#\">﹤返回首页</a>");
			out.println("</div>");
			out.println("</div>");
			out.println("</body>");
			out.println("</html>");
			response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");
			response.setHeader("Pragma", "no-cache");
			response.setDateHeader("Expires", 0);
	}

/**
   * 通知前端需要登录，包含的动作：
   * <ol>
   * <li>清空cookie中的AT和RT</li>
   * <li>如果是ajax请求，则设置状态码为401，在body中带上需要重新登录的原因，添加包含登录地址的Header(loginurl)</li>
   * <li>如果是非ajax请求，添加Location头</li>
   * <li>调用认证失败的切面接口</li>
   * </ol>
   */
  public void redirectToLoginPage(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain, String errorType, String errorDescription) throws IOException,
      ServletException {
    // 清空cookie中的AT和RT
    CookieUtil.removeCookie(request, response, CookieUtil.SFS_COOKIENAME_ACCESSTOKEN);
    CookieUtil.removeCookie(request, response, CookieUtil.SFS_COOKIENAME_REFRESHTOKEN);

    String loginUrl = oAuth2SSOService.caculateRedirectLoginUrl(request);
    if (HeaderUtil.isAJAXRequest(request) || HeaderUtil.isAPIRequest(request)) {
      // ajax请求或api网关转发的请求，返回设置状态码为401
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

      String userAgent = request.getHeader("User-Agent").toLowerCase();
      if ((userAgent.contains("msie") || (userAgent.contains("gecko") && userAgent
          .contains("rv:11")))) {
        response.setContentType("text/plain;charset=UTF-8");
      } else {
        response.setContentType("application/json;charset=UTF-8");
      }

      // 在body中带上错误的类型和描述
      JSONObject jsonObject = new JSONObject();
      jsonObject.put("errorType", errorType);
      jsonObject.put("errorDescription", errorDescription);
      response.getOutputStream().write(jsonObject.toString().getBytes("UTF-8"));

      // 加上兼容平台前端的Header(c2login)
      response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");
      response.setHeader("Pragma", "no-cache");
      response.setDateHeader("Expires", 0);
      response.setHeader("nologin", "c2login");
      response.setHeader("loginurl", loginUrl);
    } else {
      //页面请求
      String base = request.getContextPath();
      String currentURL = request.getRequestURI();
      if (!StringUtils.isEmpty(base) && !StringUtils.isEmpty(currentURL) && !"/".equals(base)) {
        //有上下文则移除之，在计算loginUrl时已经有了
        currentURL = currentURL.substring(base.length());
      }
      String queryStr = request.getQueryString();
      if (!StringUtils.isEmpty(queryStr)) {
        currentURL += "?" + URLDecoder.decode(queryStr, "utf-8");
      }
      if (!StringUtils.isEmpty(currentURL) && !currentURL.equals("/")) {
        loginUrl += "?backUrl=" + URLEncoder.encode(currentURL, "utf-8");
      }
      // 浏览器地址栏发起的请求，返回307
      response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
      response.setHeader("Location", loginUrl);
    }

    CredentialConfiguration.getAuthrizationSpi().onTokenCheckFailed(request, response, chain,
        loginUrl);
  }

  /**
   * 禁止访问，返回错误信息
   */
  public void accessDenied(ServletResponse response, String errMessage) {
    try {
      response.setContentType("text/html;charset=UTF-8");
      response.getWriter().write(errMessage);
      ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * 处理登录过程发生的错误，返回403，同时在响应的页面上显示错误信息
   *
   * @param errMessage 显示的错误信息
   * @param redirectUrl 重定向地址，如果不为空，则浏览器5秒后会自动重定向
   */
  public void handleLoginError(ServletResponse response,
      String errMessage, String redirectUrl) throws IOException {
    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().write(errMessage);
    ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
    if (null != redirectUrl && !redirectUrl.trim().equals("")) {
      ((HttpServletResponse) response).setHeader("refresh", "5;url=" + redirectUrl);
      response.getWriter()
          .write("<br/><br/>5秒后自动尝试重新登录，您也可以点击<a href='" + redirectUrl + "'>这里</a>立即重新登录");
    }
  }

  /**
   * 获取RT的maxAge，安全等级为普通时，返回服务端指定的maxAge，其他情况为-1(有效期与session一致）
   */
  private int getRTMaxAge(OAuth2Credential credential) {
    return SecurityLevel.NORMAL.equals(credential.getSecurityLevel()) ? credential
        .innerGetReExpiresInSeconds().intValue() : -1;
  }

  @Override
  public void destroy() {
  }

}
